﻿using System;
using System.Collections.Generic;
using System.Text;

namespace Vortex.Drawing {

	//part of canvas which is responsible for drawing styled primitives etc
	/// <summary>
	/// 
	/// </summary>
	public sealed partial class Canvas2D {
		private const int STROKES_IN_TEXTURE = 8;
		private static Texture _lineStrokesTexture;
		private float _strokePosition;
		///<summary>Internal flag which allows keep line stroke on drawing line</summary>
		private bool _preserveStroke;

		/// <summary>
		/// Loads the line strokes.
		/// </summary>
		internal static void LoadLineStrokes() {
			_lineStrokesTexture = new Texture(@"Vortex.Drawing.Files.line_strokes.png");

			SpriteGrid grid = _lineStrokesTexture.ToGrid(1, STROKES_IN_TEXTURE);
			LineStroke.Solid = new LineStroke(grid[0]);
			LineStroke.Dual = new LineStroke(grid[1]);
			LineStroke.Smooth = new LineStroke(grid[2]);
			LineStroke.TinyDashed = new LineStroke(grid[3]);
			LineStroke.Dashed = new LineStroke(grid[4]);
			LineStroke.CombinedDashed = new LineStroke(grid[5]);
			LineStroke.DotDashed = new LineStroke(grid[6]);
			LineStroke.Dotted = new LineStroke(grid[7]);
		}

		/// <summary>
		/// Frees the line strokes.
		/// </summary>
		internal static void FreeLineStrokes() {
			LineStroke.Solid = null;
			LineStroke.Dual = null;
			LineStroke.Smooth = null;
			LineStroke.TinyDashed = null;
			LineStroke.Dashed = null;
			LineStroke.CombinedDashed = null;
			LineStroke.DotDashed = null;
			LineStroke.Dotted = null;

			_lineStrokesTexture.Dispose();
		}

		/// <summary>
		/// Draws the line with specified style.
		/// </summary>
		/// <param name="startX">The start X coord of line.</param>
		/// <param name="startY">The start Y coord of line.</param>
		/// <param name="endX">The end X coord of line.</param>
		/// <param name="endY">The end Y coord of line.</param>
		/// <param name="style">The style to use for line shading.</param>
		/// <param name="colorTint">The color tint.</param>
		public void DrawLine(float startX, float startY, float endX, float endY, LineStyle style, ColorU colorTint) {
			DrawLine(new Vector2(startX, startY), new Vector2(endX, endY), style, colorTint);
		}

		/// <summary>
		/// Draws the line with specified style.
		/// </summary>
		/// <param name="start">The start point.</param>
		/// <param name="end">The end point.</param>
		/// <param name="style">The style to use for line shading.</param>
		/// <param name="colorTint">The color tint.</param>
		public void DrawLine(Vector2 start, Vector2 end, LineStyle style, ColorU colorTint) {
			if (null == style) throw new ArgumentNullException("style");

			if (!_preserveStroke) {
				_strokePosition = 0.0f;
			}

			Transform2D.Multiply(ref _transform, ref start, out start);
			Transform2D.Multiply(ref _transform, ref end, out end);

			Vector2 lineVector;
			Vector2.Subtract(ref end, ref start, out lineVector);
			float lineLength = lineVector.Magnitude;

			if (lineLength > 0) {

				Vector2 lineNormalizedVector = lineVector * (1 / lineLength);
				Vector2 crossVector = new Vector2(lineNormalizedVector.Y, -lineNormalizedVector.X) * style.Width;
				//adjust end, empiric
				//end += lineNormalizedVector;

				LineStroke stroke = style.Stroke;
				Sprite strokeSprite = stroke.PatternProvider.ToSprite();
				Rect coords = strokeSprite.TransformedRegion;

				//set canvas and texture at once
				_renderer.SetCanvas(this, strokeSprite.Texture);

				unsafe {
					FragmentLocation location;
					_renderer.AllocateFragment(VertexFragment.Quad, out location);
					Vertex2D* vertices = location.Vertices;

					vertices[0].XY = start + crossVector;
					vertices[0].Z = _Z;
					vertices[0].Color = colorTint;
					vertices[0].UV1 = new Vector2(_strokePosition, coords.Bottom);

					vertices[1].XY = start - crossVector;
					vertices[1].Z = _Z;
					vertices[1].Color = colorTint;
					vertices[1].UV1 = new Vector2(_strokePosition, coords.Top);

					//move stroke position
					_strokePosition += (lineLength / (style.Width * 2) * strokeSprite.Size.Y) * strokeSprite.Texture.InvSize.X;

					vertices[2].XY = end - crossVector;
					vertices[2].Z = _Z;
					vertices[2].Color = colorTint;
					vertices[2].UV1 = new Vector2(_strokePosition, coords.Top);

					vertices[3].XY = end + crossVector;
					vertices[3].Z = _Z;
					vertices[3].Color = colorTint;
					vertices[3].UV1 = new Vector2(_strokePosition, coords.Bottom);

					PopulateQuadIndices(ref location);
				}
			}
		}

		/// <summary>
		/// Draws the rect with styled lines.
		/// </summary>
		/// <param name="x">The x of left top rect coord.</param>
		/// <param name="y">The y of left top rect coord.</param>
		/// <param name="width">The rect width.</param>
		/// <param name="height">The rect height.</param>
		/// <param name="style">The style to use for shading lines.</param>
		/// <param name="colorTint">The color tint.</param>
		public void DrawRect(float x, float y, float width, float height, LineStyle style, ColorU colorTint) {
			DrawRect(Rect.FromBox(x, y, width, height), style, colorTint);
		}

		/// <summary>
		/// Draws the rect with styled lines.
		/// </summary>
		/// <param name="rect">The rectangle.</param>
		/// <param name="style">The style to use for shading lines.</param>
		/// <param name="colorTint">The color tint.</param>
		public void DrawRect(Rect rect, LineStyle style, ColorU colorTint) {
					
			DrawLine(rect.LeftTop, rect.RightTop, style, colorTint);
			DrawLine(rect.RightTop, rect.RightBottom, style, colorTint);
			DrawLine(rect.RightBottom, rect.LeftBottom, style, colorTint);
			DrawLine(rect.LeftBottom, rect.LeftTop, style, colorTint);
			
		}

		/// <summary>
		/// Draws the styled ellipse in specified bounding rect.
		/// </summary>
		/// <param name="x">The x of left top rect point.</param>
		/// <param name="y">The y of left top rect point.</param>
		/// <param name="width">The width of rect.</param>
		/// <param name="height">The height of rect.</param>
		/// <param name="style">The style to use for shading.</param>
		/// <param name="colorTint">The color tint.</param>
		public void DrawEllipse(float x, float y, float width, float height, LineStyle style, ColorU colorTint) {
			DrawEllipse(Rect.FromBox(x, y, width, height), style, colorTint);
		}

		/// <summary>
		/// Draws the styled ellipse in specified bounding rect.
		/// </summary>
		/// <param name="rect">The rectangle.</param>
		/// <param name="style">The style to use for shading lines.</param>
		/// <param name="colorTint">The color tint.</param>
		public void DrawEllipse(Rect rect, LineStyle style, ColorU colorTint) {
			DrawPolygon(PrepareEllipseOffsets(rect), style, colorTint); 
			/*
			Vector2[] vertices = PrepareEllipseOffsets(rect);
			DrawPolylineExperimental(vertices, 0, vertices.Length, style, colorTint, true); 
			*/
		}

		/// <summary>
		/// Draws the styled polyline.
		/// </summary>
		/// <param name="points">The array with points points of polyline.</param>
		/// <param name="style">The style to use for shading.</param>
		/// <param name="colorTint">The color tint.</param>
		public void DrawPolyline(Vector2[] points, LineStyle style, ColorU colorTint) {
			DrawPolyline(points, 0, -1, style, colorTint);
		}

		/// <summary>
		/// Draws the styled polyline.
		/// </summary>
		/// <param name="points">The array with points of polyline.</param>
		/// <param name="startElement">The start element in array to use.</param>
		/// <param name="elementCount">The element count to use.</param>
		/// <param name="style">The style to use for shading.</param>
		/// <param name="colorTint">The color tint.</param>
		public void DrawPolyline(Vector2[] points, int startElement, int elementCount, LineStyle style, ColorU colorTint) {
			DrawPolyline(points, startElement, elementCount, style, colorTint, false);
		}

		/// <summary>
		/// Draws the styled polylygon.
		/// </summary>
		/// <param name="points">The array with points points of polyline.</param>
		/// <param name="style">The style to use for shading.</param>
		/// <param name="colorTint">The color tint.</param>
		public void DrawPolygon(Vector2[] points, LineStyle style, ColorU colorTint) {
			DrawPolygon(points, 0, -1, style, colorTint);
		}

		/// <summary>
		/// Draws the styled polygon.
		/// </summary>
		/// <param name="points">The array with points of polyline.</param>
		/// <param name="startElement">The start element in array to use.</param>
		/// <param name="elementCount">The element count to use.</param>
		/// <param name="style">The style to use for shading.</param>
		/// <param name="colorTint">The color tint.</param>
		public void DrawPolygon(Vector2[] points, int startElement, int elementCount, LineStyle style, ColorU colorTint) {
			DrawPolyline(points, startElement, elementCount, style, colorTint, true);
		}

		#region Better Polyline Drawing

		///<summary>Help structure with precalculated </summary>
		struct ThickLineContext {
			///<summary>Current offset of stroke in 0..1 scale</summary>
			public float StrokeOffset;
			///<summary>Stroke factor helps to get stroke offset change based on segment length</summary>
			public float StrokeFactor;
			///<summary>Top coordinate of stroke sprite</summary>
			public float TopCoord;
			///<summary>Center coordinate of stroke sprite</summary>
			public float CenterCoord;
			///<summary>Bottom coordinate of stroke sprite</summary>
			public float BottomCoord;
			///<summary>Width of line</summary>
			public float LineWidth;
			///<summary>Half of width of line</summary>
			public float LineHalfWidth;
		}

		public void DrawPolylineExperimental(Vector2[] points, int startElement, int elementCount, LineStyle style, ColorU colorTint, bool joinFirstAndLast) {
			_renderer.SetCanvas(this, style.Stroke.PatternProvider.ToSprite().Texture);

			int segmentCount = elementCount - (joinFirstAndLast ? 0 : 1);
			//1. Set Canvas, transform points???
			//2. Fill Vertex segments
			VertexFragment fragment = new VertexFragment(segmentCount * 5, segmentCount * 9 - (joinFirstAndLast ? 0 : 3), PrimitiveType.TriangleList);

			//get all of stuff
			LineStroke stroke = style.Stroke;
			Sprite strokeSprite = stroke.PatternProvider.ToSprite();
			Rect coords = strokeSprite.TransformedRegion;

			ThickLineContext context = new ThickLineContext();
			context.TopCoord = coords.Top;
			context.CenterCoord = coords.CenterY;
			context.BottomCoord = coords.Bottom;
			context.StrokeFactor = coords.Height / style.Width * 0.5f;
			context.LineWidth = style.Width;
			context.StrokeOffset = -style.Width * context.StrokeFactor * 0.5f;
			context.LineHalfWidth = style.Width * 0.5f;

			unsafe {
				FragmentLocation location;
				_renderer.AllocateFragment(fragment, out location);

				//-------------------------------------------------------------

				//ok, lets add all of segments
				for (int n = 0; n < elementCount - 1; ++n) {
					AddPolylineSegment(ref location, ref context, n, points[startElement + n], points[startElement + n + 1], colorTint);
				}
				if (joinFirstAndLast) {
					AddPolylineSegment(ref location, ref context, elementCount - 1, points[startElement + elementCount - 1], points[startElement], colorTint);
				}

				//-------------------------------------------------------------

				//precalculate joint indice pointer
				ushort* jointIndices = location.Indices + segmentCount * 6;

				//go through all : join polyline segment
				for (int n = 0; n < elementCount - 1; ++n) {
					AddPolylineSegmentJoint(jointIndices, location.Vertices, location.BaseIndex, n, n + 1);
					jointIndices += 3;
				}
				//if required, lets join lat polygon segment
				if (joinFirstAndLast) {
					AddPolylineSegmentJoint(jointIndices, location.Vertices, location.BaseIndex, elementCount - 1, 0);
				}
			}

		}

		/// <summary>
		/// Adds the polyline segment.
		/// </summary>
		/// <param name="location">The location.</param>
		/// <param name="segment">The segment ordinal number.</param>
		/// <param name="start">The start point (already transformed).</param>
		/// <param name="end">The end point (already transformed).</param>
		private unsafe void AddPolylineSegment(ref FragmentLocation location, ref ThickLineContext context, int segment, Vector2 start, Vector2 end, ColorU color) {
			Vertex2D* vertices = location.Vertices + segment * 5;
			ushort* indices = location.Indices + segment * 6;
			int baseIndex = location.BaseIndex + segment * 5;

			//calculate distance between
			Vector2 deltaVector = end - start;
			float length = deltaVector.Length();
			Vector2 normalizedDeltaVector = deltaVector * (1 / length);
			Vector2 normalizedCrossVector = new Vector2(normalizedDeltaVector.Y, -normalizedDeltaVector.X);

			//fill first PAIR
			if (length > 0) {
				//make some safe offsets...
				Vector2 crossVector = normalizedCrossVector * context.LineWidth;

				//fill vertices
				vertices[0].XY = start + crossVector;
				vertices[0].Z = _Z;
				vertices[0].Color = color;
				vertices[0].UV1 = new Vector2(context.StrokeOffset, context.TopCoord);

				vertices[1].XY = start - crossVector;
				vertices[1].Z = _Z;
				vertices[1].Color = color;
				vertices[1].UV1 = new Vector2(context.StrokeOffset, context.BottomCoord);

				//promote stroke offset
				context.StrokeOffset += context.StrokeFactor * length;

				vertices[2].XY = end + crossVector;
				vertices[2].Z = _Z;
				vertices[2].Color = color;
				vertices[2].UV1 = new Vector2(context.StrokeOffset, context.TopCoord);

				vertices[3].XY = end - crossVector;
				vertices[3].Z = _Z;
				vertices[3].Color = color;
				vertices[3].UV1 = new Vector2(context.StrokeOffset, context.BottomCoord);

				//add last fifth vertex (central one)
				vertices[4].XY = end;
				vertices[4].Z = _Z;
				vertices[4].Color = color;
				vertices[4].UV1 = new Vector2(context.StrokeOffset, context.CenterCoord);

			} else {
				; //???
			}

			//fill indices
			indices[0] = (ushort)baseIndex;
			indices[1] = (ushort)(baseIndex + 2);
			indices[2] = (ushort)(baseIndex + 1);

			indices[3] = (ushort)(baseIndex + 1);
			indices[4] = (ushort)(baseIndex + 2);
			indices[5] = (ushort)(baseIndex + 3);
		}

		/// <summary>
		/// Adds the polyline segment joint into index array.
		/// </summary>
		/// <param name="jointIndices">The joint indices pointer.</param>
		/// <param name="baseIndex">Index of the segment vertex base.</param>
		/// <param name="segment1">The segment1 ordinal number.</param>
		/// <param name="segment2">The segment2 ordinal number.</param>
		private unsafe void AddPolylineSegmentJoint(ushort* jointIndices, Vertex2D* vertices, int baseIndex, int segment1, int segment2) {
			//add first joint tri
			int index1 = baseIndex + segment1 * 5;
			int index2 = baseIndex + segment2 * 5;

			Vector2 d1 = vertices[index1 + 2].XY - vertices[index1].XY;
			Vector2 d2 = vertices[index2 + 2].XY - vertices[index2].XY;

			float a = d1.Dot(d2);
			float b = new Vector2(-d1.Y, d1.X).Dot(d2);

			float angle = (float)Math.Atan2(a, b);
			if (angle < 1.61f && angle > -1.61f) {
				jointIndices[0] = (ushort)(index1 + 2);
				jointIndices[1] = (ushort)(index2);
				jointIndices[2] = (ushort)(index1 + 4);
			} else {
				jointIndices[0] = (ushort)(index1 + 3);
				jointIndices[1] = (ushort)(index2 + 1);
				jointIndices[2] = (ushort)(index1 + 4);
			}
		}


		#endregion


		#region Internal methods

		/// <summary>
		/// Internal implementation of drawing the polyline/polygon with style.
		/// </summary>
		/// <param name="points">The points array.</param>
		/// <param name="startElement">The start element in array.</param>
		/// <param name="elementCount">The element count.</param>
		/// <param name="style">The style to use for shading.</param>
		/// <param name="connectLastWithFirst">if set to <c>true</c> then last point will be connected with first one.</param>
		/// <param name="colorTint">The color tint.</param>
		private void DrawPolyline(Vector2[] points, int startElement, int elementCount, LineStyle style, ColorU colorTint, bool connectLastWithFirst) {
			if (null == points) throw new ArgumentNullException("points");			

			if (startElement < 0 || startElement >= points.Length) {
				throw new ArgumentException("startElement", "Start element is out of array index");
			}

			if (elementCount == -1) {
				elementCount = points.Length - startElement;
			} else {
				if (startElement + elementCount > points.Length) {
					throw new ArgumentException("elementCount", "End element is out of array index");
				}
			}

			if (elementCount > 1) {
				BeginStroke();

				for (int n = 0; n < elementCount - 1; ++n) {
					DrawLine(points[n + startElement], points[n + startElement + 1], style, colorTint);
				}

				if (connectLastWithFirst) {
					DrawLine(points[startElement + elementCount - 1], points[startElement], style, colorTint);
				}

				EndStroke();
			}
		}

		/// <summary>
		/// Ends the stroke session (disables stroke position preserving).
		/// </summary>
		private void EndStroke() {
			_preserveStroke = false;
		}

		/// <summary>
		/// Begins the stroke session (reset stroke position and enables preserving of position between line draw calls).
		/// </summary>
		private void BeginStroke() {
			_preserveStroke = true;
			_strokePosition = 0;
		}

		#endregion

	}
}
